﻿// ------------------------------------------------------------------------------
// <copyright file="TimeScroller.cs" company="skumaronline.com">
//    Copyright (C) 2012 Suresh Kumar T.
//
//    This program is free software; you can redistribute it and/or
//    modify it under the terms of the GNU General Public License
//    as published by the Free Software Foundation version 2
//    of the License.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    http://www.gnu.org/licenses/gpl-2.0.html
//
// </copyright>
// ------------------------------------------------------------------------------

namespace StoreAppLib.Controls
{
    using System;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Media.Animation;

    /// <summary>
    /// Time scroll class for hours, minutes and seconds
    /// </summary>
    public sealed class TimeScroller : Control
    {
        /// <summary>
        /// Maximum value property
        /// </summary>
        public static readonly DependencyProperty MaxValueSetProperty =
            DependencyProperty.Register("MaxValue", typeof(int), typeof(TimeScroller), new PropertyMetadata(0, OnTimesPropertyChanged));

        /// <summary>
        /// Minimum value property
        /// </summary>
        public static readonly DependencyProperty MinValueSetProperty =
            DependencyProperty.Register("MinValue", typeof(int), typeof(TimeScroller), new PropertyMetadata(0, OnTimesPropertyChanged));

        /// <summary>
        /// Selected or default value property
        /// </summary>
        public static readonly DependencyProperty ValueSetProperty =
            DependencyProperty.Register("Value", typeof(int), typeof(TimeScroller), new PropertyMetadata(0, OnTimesPropertyChanged));

        /// <summary>
        /// Time format property
        /// </summary>
        public static readonly DependencyProperty FormatProperty =
            DependencyProperty.Register("Format", typeof(string), typeof(TimeScroller), new PropertyMetadata("00", OnTimesPropertyChanged));

        /// <summary>
        /// Corner radius property
        /// </summary>
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(double), typeof(TimeScroller), new PropertyMetadata(0));

        /// <summary>
        /// Scroll viewer vertical offset property
        /// </summary>
        internal static readonly DependencyProperty TimeVerticalOffSetProperty =
               DependencyProperty.Register("TimeVerticalOffSet", typeof(double), typeof(TimeScroller), new PropertyMetadata(0.0, OnTimeVerticalOffSetPropertyChanged));

        /// <summary>
        /// The scroll viewer
        /// </summary>
        private ScrollViewer scroller;

        /// <summary>
        /// Whether scroll viewer is scrolling
        /// </summary>
        private bool isScrolling = false;

        /// <summary>
        /// Whether scroll process completed
        /// </summary>
        private bool isScrollCompleteProcessing = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="TimeScroller" /> class
        /// </summary>
        public TimeScroller()
        {
            this.DefaultStyleKey = typeof(TimeScroller);

            this.Times = new ObservableCollection<string>();

            this.DataContext = this;

            this.Loaded += this.OnTimeScrollerLoaded;
        }

        /// <summary>
        /// Value changed event handler
        /// </summary>
        public event ValueChangedEventHandler ValueChanged;

        /// <summary>
        /// Gets data source
        /// </summary>
        public ObservableCollection<string> Times { get; private set; }

        /// <summary>
        /// Gets or sets corner radius
        /// </summary>
        public double CornerRadius
        {
            get { return (double)this.GetValue(CornerRadiusProperty); }
            set { this.SetValue(CornerRadiusProperty, value); }
        }

        /// <summary>
        /// Gets or sets time format
        /// </summary>
        public string Format
        {
            get { return (string)this.GetValue(FormatProperty); }
            set { this.SetValue(FormatProperty, value); }
        }

        /// <summary>
        /// Gets or sets selected or default value
        /// </summary>
        public int Value
        {
            get { return (int)this.GetValue(ValueSetProperty); }
            set { this.SetValue(ValueSetProperty, value); }
        }

        /// <summary>
        /// Gets or sets minimum value
        /// </summary>
        public int MinValue
        {
            get { return (int)this.GetValue(MinValueSetProperty); }
            set { this.SetValue(MinValueSetProperty, value); }
        }

        /// <summary>
        /// Gets or sets maximum value
        /// </summary>
        public int MaxValue
        {
            get { return (int)this.GetValue(MaxValueSetProperty); }
            set { this.SetValue(MaxValueSetProperty, value); }
        }

        /// <summary>
        /// Gets or sets the scroll viewer vertical offset
        /// </summary>
        internal double TimeVerticalOffSet
        {
            get { return (double)this.GetValue(TimeVerticalOffSetProperty); }
            set { this.SetValue(TimeVerticalOffSetProperty, value); }
        }

        /// <summary>
        /// Occurs when Time Vertical Offset property is set
        /// </summary>
        /// <param name="sender">The source</param>
        /// <param name="args">The property changed event argument</param>
        internal static void OnTimeVerticalOffSetPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            if (args.NewValue != args.OldValue)
            {
                TimeScroller thisInstance = sender as TimeScroller;
                thisInstance.ScrollToVerticalOffset((double)args.NewValue);
            }
        }

        /// <summary>
        /// Occurs when time minimum, maximum or value property is set
        /// </summary>
        /// <param name="sender">The source</param>
        /// <param name="args">The property changed event argument</param>
        internal static void OnTimesPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            if (args.OldValue != args.NewValue)
            {
                TimeScroller thisInstance = sender as TimeScroller;
                thisInstance.InitializeTimes();
            }
        }

        /// <summary>
        /// Scroll the scroll viewer to specified offset
        /// </summary>
        /// <param name="scrollOffSet">The scroll offset</param>
        internal void ScrollToVerticalOffset(double scrollOffSet)
        {
            if (this.scroller != null)
            {
                this.scroller.ScrollToVerticalOffset(scrollOffSet);
            }
        }

        /// <summary>
        /// Scroll programmatically
        /// </summary>
        /// <param name="down">Up or down</param>
        internal void Scroll(bool down)
        {
            lock (this)
            {
                this.ScrollToVerticalOffset(this.scroller.VerticalOffset + (down ? 50.0 : -50.0));

                this.OnTimeScrolled(this.scroller, new ScrollViewerViewChangedEventArgs());
            }
        }

        /// <summary>
        ///  Invoked whenever application code or internal processes (such as a rebuilding
        ///  layout pass) call ApplyTemplate. In simplest terms, this means the method
        ///  is called just before a UI element displays in your app. 
        /// </summary>
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this.scroller = this.GetTemplateChild("scroller") as ScrollViewer;

            this.scroller.ViewChanged += this.OnTimeScrolled;
        }

        /// <summary>
        /// Initialize the time values
        /// </summary>
        private void InitializeTimes()
        {
            //// If scrolling is processing do nothing
            if (this.isScrollCompleteProcessing)
            {
                return;
            }

            this.Times.Clear();

            int middle = (int)Math.Round((this.MinValue - this.MaxValue) / 2.0);

            int difference = middle - this.Value + this.MinValue;

            for (int i = this.MinValue; i <= this.MaxValue; i++)
            {
                this.Times.Add(i.ToString(this.Format));
            }

            while (difference < 0)
            {
                string item = this.Times[0];
                this.Times.RemoveAt(0);
                this.Times.Add(item);
                difference++;
            }

            while (difference > 0)
            {
                string item = this.Times[this.Times.Count - 1];
                this.Times.RemoveAt(this.Times.Count - 1);
                this.Times.Insert(0, item);
                difference--;
            }
        }

        /// <summary>
        /// Scroll to center on load
        /// </summary>
        /// <param name="sender">The source</param>
        /// <param name="e">The argument</param>
        private void OnTimeScrollerLoaded(object sender, RoutedEventArgs e)
        {
            this.TimeVerticalOffSet = (Math.Round(this.Times.Count / 2.0) * 50) - 50;
        }

        /// <summary>
        /// Occurs when scroll viewer is scrolled
        /// </summary>
        /// <param name="sender">The scroll viewer</param>
        /// <param name="e">Scroll change event argument</param>
        private void OnTimeScrolled(object sender, ScrollViewerViewChangedEventArgs e)
        {
            if (this.isScrollCompleteProcessing)
            {
                return;
            }

            if (!e.IsIntermediate)
            {
                ScrollViewer scrollViewer = sender as ScrollViewer;
                double offSet = scrollViewer.VerticalOffset % 50.0;

                if (offSet == 0)
                {
                    int currentIndex = (int)Math.Round((scrollViewer.VerticalOffset / 50.0) + 1);

                    double middle = Math.Round(this.Times.Count / 2.0);

                    if (currentIndex != middle)
                    {
                        this.isScrollCompleteProcessing = true;

                        this.Value = int.Parse(this.Times[currentIndex]);

                        if (this.ValueChanged != null)
                        {
                            this.ValueChanged(this, this.Value);
                        }

                        while (middle < currentIndex)
                        {
                            string item = this.Times[0];
                            this.Times.RemoveAt(0);
                            this.Times.Add(item);
                            middle++;
                        }

                        while (middle > currentIndex)
                        {
                            string item = this.Times[this.Times.Count - 1];
                            this.Times.RemoveAt(this.Times.Count - 1);
                            this.Times.Insert(0, item);
                            middle--;
                        }

                        scrollViewer.ScrollToVerticalOffset((Math.Round(this.Times.Count / 2.0) * 50.0) - 50);
                    }
                }

                this.isScrollCompleteProcessing = false;

                if (offSet != 0 && !this.isScrolling)
                {
                    this.isScrolling = true;
                    this.RenderTransform = new CompositeTransform();

                    Storyboard storyboard = new Storyboard();

                    DoubleAnimation animation = new DoubleAnimation();
                    animation.EnableDependentAnimation = true;
                    animation.Duration = TimeSpan.FromSeconds(0.25);
                    animation.From = scrollViewer.VerticalOffset;
                    if (offSet > 25)
                    {
                        animation.To = Math.Round(scrollViewer.VerticalOffset + 50 - offSet, 0);
                    }
                    else
                    {
                        animation.To = Math.Round(scrollViewer.VerticalOffset - offSet, 0);
                    }

                    storyboard.Children.Add(animation);

                    Storyboard.SetTarget(animation, this);
                    Storyboard.SetTargetProperty(animation, "(TimeScroller.TimeVerticalOffSet)");

                    storyboard.Begin();
                }
            }
            else
            {
                this.isScrolling = false;
            }
        }
    }
}
